﻿using codeparser.net.v2.Aspects;
using codeparser.net.v2.Entities;
using codeparser.net.v2.Exceptions;
using codeparser.net.v2.Lexer;
using codeparser.net.v2.Validators;

using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;

namespace codeparser.net.v2.Analyzer
{
    /// <summary>
    /// Represents the analyzer.
    /// </summary>
    internal class Analyzer
    {
        /// <summary>
        /// Contains the tag configuration.
        /// </summary>
        private TagConfiguration _tagConfiguration;

        /// <summary>
        /// Gets or sets the token validator strategy.
        /// </summary>
        /// <value>The token validator strategy.</value>
        internal TokenValidatorStrategyBase TokenValidatorStrategy
        {
            get;
            set;
        }

        /// <summary>
        /// Creates a parse tree out of the specified list of tokens.
        /// </summary>
        /// <param name="tokens"></param>
        /// <returns>The root of the parse tree.</returns>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        [LoggingAspect]
        internal ITreeNode CreateParseTree(IList<Token> tokens)
        {
            // Validate the tokens parameter.
            if (tokens == null)
            {
                throw new ArgumentNullException("tokens");
            }

            // If there are no tokens given, return an empty text.
            if (tokens.Count == 0)
            {
                return new TreeNodeText(null, "");
            }

            // Create the root node.
            ITreeNode root = new TreeNodeTag(null, null, this._tagConfiguration);
            ITreeNode currentNode = root;

            // Iterate over all tokens.
            foreach (Token token in tokens)
            {
                switch (token.Type)
                {
                    case TokenType.Element:
                        // The token represents a closing tag, so check whether there is a matching
                        // opening tag. If not, throw an exception.
                        if (token.Value.StartsWith("/", StringComparison.CurrentCulture))
                        {
                            string closingTag = "/" + token.Tag.Element.Name;

                            // If there is a closing tag without an opening tag, throw an
                            // exception.
                            if (currentNode.Parent == null)
                            {
                                throw new MissingOpeningTagException(String.Format(CultureInfo.CurrentCulture,
                                    "Missing opening tag for '{0}'", closingTag));
                            }

                            string openingTag = ((TreeNodeTag)currentNode).Tag.Element.Name;

                            // If there are matching opening and closing tags, move one level up.
                            // Otherwise throw an exception.
                            if (openingTag == closingTag.TrimStart('/'))
                            {
                                currentNode = currentNode.Parent;
                            }
                            else
                            {
                                throw new InvalidTagNestingException(String.Format(CultureInfo.CurrentCulture,
                                    "Invalid tag nesting with '{0}' and '{1}'", openingTag, closingTag));
                            }
                            break;
                        }

                        // The token represents an opening tag, so add a new node.
                        TreeNodeTag newNode = new TreeNodeTag(currentNode, token.Tag, this._tagConfiguration);
                        ((TreeNodeTag)currentNode).Nodes.Add(newNode);

                        // If the token is a pair tag, set the new node as current node, otherwise
                        // leave the current node as it is.
                        if (token.Tag.ClosingTagStyle == TagClosingStyle.Required)
                        {
                            currentNode = newNode;
                        }
                        break;
                    case TokenType.Attribute:
                        // Check whether the attribute is a valid attribute for the current
                        // element.
                        TagAttribute attribute;
                        this.TokenValidatorStrategy.ValidateAttribute(
                            token, ((TreeNodeTag)currentNode).Tag, out attribute);

                        // Add the current attribute to the current element.
                        ((TreeNodeTag)currentNode).Attributes.Add(attribute);
                        break;
                    case TokenType.AttributeValue:
                        // Check if an attribute has yet been added. If not, add the default
                        // attribute.
                        if (((TreeNodeTag)currentNode).Attributes.Count == 0)
                        {
                            foreach (TagAttribute tagAttribute in ((TreeNodeTag)currentNode).Tag.Attributes)
                            {
                                if ((tagAttribute.Style & AttributeStyles.Default) == AttributeStyles.Default)
                                {
                                    ((TreeNodeTag)currentNode).Attributes.Add(tagAttribute);
                                    break;
                                }
                            }
                        }

                        // Add the value to the latest added attribute.
                        ((TreeNodeTag)currentNode).Attributes[((TreeNodeTag)currentNode).Attributes.Count - 1].Value =
                            token.Value;
                        break;
                    case TokenType.Text:
                        // Add the text as new text node.
                        ((TreeNodeTag)currentNode).Nodes.Add(new TreeNodeText(currentNode, token.Value));
                        break;
                    default:
                        break;
                }
            }

            // If the current node is not the root node in the end, there is still a tag missing.
            if (currentNode != root)
            {
                throw new MissingClosingTagException(String.Format(CultureInfo.CurrentCulture,
                    "Missing closing tag for '{0}'", ((TreeNodeTag)currentNode).Tag.Element.Name));
            }

            // Check that no required attribute is missing.
            this.ValidateRequiredAttributes((TreeNodeTag)root);

            // Check the value styles of the attributes.
            this.ValidateAttributeValueStyles((TreeNodeTag)root);

            // Return the root of the parse tree to the caller.
            return root;
        }

        /// <summary>
        /// Validates that the value styles are paid attention to.
        /// </summary>
        /// <param name="node">The node.</param>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        [LoggingAspect]
        private void ValidateAttributeValueStyles(TreeNodeTag node)
        {
            // Iterate over all attributes.
            foreach (TagAttribute attribute in node.Attributes)
            {
                // Check whether the given attribute value matches the attribute value style. If
                // not, throw an exception.
                switch (attribute.ValueStyle)
                {
                    case AttributeValueStyles.None:
                        if (attribute.Value != null)
                        {
                            throw new NeedlessAttributeValueSpecifiedException(String.Format(CultureInfo.CurrentCulture,
                                "Needless value for attribute '{0}' specified", attribute.Name));
                        }
                        break;
                    case AttributeValueStyles.Optional:
                        break;
                    case AttributeValueStyles.Required:
                        if (attribute.Value == null)
                        {
                            throw new RequiredAttributeValueMissingException(String.Format(CultureInfo.CurrentCulture,
                                "Required value for attribute '{0}' is missing", attribute.Name));
                        }
                        break;
                    default:
                        throw new UnknownAttributeValueStyleException("Unknown attribute value style");
                }
            }

            // Validate all subordinate tags.
            foreach (ITreeNode subNode in node.Nodes)
            {
                if (subNode is TreeNodeTag)
                {
                    this.ValidateAttributeValueStyles((TreeNodeTag)subNode);
                }
            }
        }

        /// <summary>
        /// Validates that there are all required attributes for the tag within the specified node.
        /// </summary>
        /// <param name="node">The node.</param>
        [SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]
        [LoggingAspect]
        private void ValidateRequiredAttributes(TreeNodeTag node)
        {
            // If there is no tag, return to the caller.
            if (node.Tag != null)
            {
                // Iterate over all of the tag's attributes.
                foreach (TagAttribute requiredAttribute in node.Tag.Attributes)
                {
                    // If the attribute is optional, skip it.
                    if ((requiredAttribute.Style & AttributeStyles.Required) != AttributeStyles.Required)
                    {
                        continue;
                    }

                    // The attribute is required, so check whether it appears in the list of actual
                    // attributes.
                    if (!node.Attributes.Contains(requiredAttribute))
                    {
                        throw new RequiredAttributeMissingException(String.Format(CultureInfo.CurrentCulture,
                            "Attribute '{0}' is missing", requiredAttribute.Name));
                    }
                }
            }

            // Validate all subordinate tags.
            foreach (ITreeNode subNode in node.Nodes)
            {
                if (subNode is TreeNodeTag)
                {
                    this.ValidateRequiredAttributes((TreeNodeTag)subNode);
                }
            }
        }

        /// <summary>
        /// Initializes a new instance of the Analyzer type.
        /// </summary>
        /// <param name="tokenValidatorStrategy">The token validator strategy.</param>
        /// <param name="tagConfiguration">The tag configuration.</param>
        [LoggingAspect]
        internal Analyzer(TokenValidatorStrategy tokenValidatorStrategy, TagConfiguration tagConfiguration)
        {
            // Initialize the fields.
            this._tagConfiguration = tagConfiguration;
            switch (tokenValidatorStrategy)
            {
                case codeparser.net.v2.Validators.TokenValidatorStrategy.CaseSensitive:
                    this.TokenValidatorStrategy = new TokenValidatorCaseSensitiveStrategy();
                    break;
                case codeparser.net.v2.Validators.TokenValidatorStrategy.CaseInsensitive:
                    this.TokenValidatorStrategy = new TokenValidatorCaseInsensitiveStrategy();
                    break;
                default:
                    throw new UnknownTokenValidatorStrategyException("Unknown token validator strategy");
            }
        }
    }
}